複数のQRコードを連続して読み取れる LIFF アプリを作ってみた
こんにちは!LINE事業部のたにもんです!
先日、以下の記事でQRコードを読み取る LIFF アプリを作成しました。
この記事で紹介した LIFF アプリでは、QRコードを1つ読み取ると「コードリーダー」が閉じてしまい、複数のQRコードを読み取りたい場合に不便でした。
そこで、複数のQRコードを効率的に読み取れる LIFF アプリを作成しました! 実際に動かした様子は次のとおりです。
以下の4つのQRコードを連続して読み取れていることが分かるかと思います!
https://www.google.com/
https://www.amazon.co.jp/
https://www.facebook.com/
https://www.apple.com/
今回開発したソースコードはこちらのリポジトリで公開しているので、実装が気になる方は参考にしてください!
環境
- node: 16.14.0
- npm: 8.3.1
- AWS CDK: 2.12.0 (build c9786db)
- iOS: 15.2.1
- LINE: 12.1.0
全体像
今回は以下のような QRCodeScanner
コンポーネントを作成することで、複数のQRコードを連続で読み取れるようにしました。
このコンポーネントでどのようにQRコードを読み取っているのかを説明していきます。
// app/src/components/QRCodeScanner.tsx import React, { useEffect, useRef, useState } from 'react'; import jsQR from 'jsqr'; const videoWidth: number = 500; const videoHeight: number = 500; const videoFrameRate: number = 5; const constraints: MediaStreamConstraints = { audio: false, video: { width: videoWidth, height: videoHeight, frameRate: { max: videoFrameRate, }, facingMode: { exact: 'environment', }, }, }; const QRCodeScanner: React.VFC = () => { const videoRef = useRef<HTMLVideoElement>(null); const intervalRef = useRef<number>(); const canvasRef = useRef<HTMLCanvasElement>(null); const [isContinue, setIsContinue] = useState(false); const [qrCodeData, setQrCodeData] = useState<string[]>([]); useEffect(() => { const openCamera = async () => { const video = videoRef.current; if (video) { const stream = await navigator.mediaDevices.getUserMedia(constraints); video.srcObject = stream; } }; openCamera(); }, []); useEffect(() => { if (!isContinue) { return; } const decodeQRCode = () => { const context = canvasRef?.current?.getContext('2d'); const video = videoRef?.current; if (!context || !video) { return; } context.drawImage(video, 0, 0, videoWidth, videoHeight); const imageData = context.getImageData(0, 0, videoWidth, videoHeight); const code = jsQR(imageData.data, videoWidth, videoHeight); return code?.data; }; const intervalId = window.setInterval(() => { const decodedValue = decodeQRCode(); if (!decodedValue || qrCodeData.includes(decodedValue)) { return; } setQrCodeData([...qrCodeData, decodedValue]); }, 1_000 / videoFrameRate); intervalRef.current = intervalId; return () => { clearInterval(intervalRef.current); }; }, [isContinue, qrCodeData]); const handleStart = () => { setIsContinue(true); }; const handleStop = () => { setIsContinue(false); }; return ( <div> <p>QR Code Scanner</p> <div style={{ display: 'grid' }}> <div> <video autoPlay playsInline={true} ref={videoRef} style={{ width: '100%' }} > <canvas width={videoWidth} height={videoHeight} ref={canvasRef} /> </video> </div> <div> <p>{qrCodeData.join('\n')}</p> </div> <div> <button onClick={handleStart}>Start Scan</button> <button onClick={handleStop}>Stop Scan</button> </div> </div> </div> ); }; export default QRCodeScanner;
カメラの起動
以下の部分では getUserMedia
メソッドを用いてカメラを起動します。
この処理は useEffect
の依存リストに空配列を指定することで、コンポーネントの初回レンダリング時のみ実行されるようにしています。
useEffect(() => { const openCamera = async () => { const video = videoRef.current; if (video) { const stream = await navigator.mediaDevices.getUserMedia(constraints); video.srcObject = stream; } }; openCamera(); }, []);
QRコードの読み取りループ
以下の部分では、setInterval
メソッドを用いて一定時間ごと(今回は200ミリ秒ごと)にQRコードの読み取りを行い、qrCodeData
配列を更新しています。
QRコードの読み取りには、後述する decodeQRCode
関数を利用しています。
なお、QRコードを読み取るか否かを isContinue
変数で制御しています。
useEffect(() => { if (!isContinue) { return; } const decodeQRCode = () => { /* 略 */ }; const intervalId = window.setInterval(() => { const decodedValue = decodeQRCode(); if (!decodedValue || qrCodeData.includes(decodedValue)) { return; } setQrCodeData([...qrCodeData, decodedValue]); }, 1_000 / videoFrameRate); intervalRef.current = intervalId; return () => { clearInterval(intervalRef.current); }; }, [isContinue, qrCodeData]);
decodeQRCode
関数
前回の記事 では、LIFF SDK v2 で提供されている scanCodeV2
メソッドを利用してQRコードを読み取りました。
この scanCodeV2
メソッドの仕様により、QRコードを1つ読み取るとコードリーダーが閉じてしまっていたので、今回は jsQR というライブラリを利用してQRコード読み取り処理を自前で書きました。
ちなみに、jsQR は scanCodeV2
メソッドの内部でも利用されています(2022年2月22日現在)。
const decodeQRCode = () => { const context = canvasRef?.current?.getContext('2d'); const video = videoRef?.current; if (!context || !video) { return; } context.drawImage(video, 0, 0, videoWidth, videoHeight); const imageData = context.getImageData(0, 0, videoWidth, videoHeight); const code = jsQR(imageData.data, videoWidth, videoHeight); return code?.data; };
QRコード読み取りの開始・終了
isContinue
変数に true
/ false
を設定する関数をそれぞれ作成し、これらを Start Scan
/ Stop Scan
というボタンの Click
イベントハンドラとして登録することで、QRコード読み取りの開始・終了を制御しています。
const handleStart = () => { setIsContinue(true); }; const handleStop = () => { setIsContinue(false); };
参考
QRコードは株式会社デンソーウェーブの登録商標です。